/**
 *  @author: Paulo Faria Reis
 *  @started: Mar 24, 2012
 */
package controller;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Iterator;
import controller.HashTypes;

/**
 * @author Paulo Faria Reis
 *
 */
public class DataStore  {
   private boolean working = true;
   private boolean toDebug = false;	// Debug mode inactive.
   private int controlPort = 8967;	// Control port (already with default value).
   private int dataPort = 8966;	// Data port (already with default value).
   private InetAddress mcAddress = null;	// Multicast address (already with default value).
   private InetAddress localInetAddr = null;
   private HashTypes hashType = HashTypes.SHA256;
   private File sharedDir = new File(".");
   private String currentSearch;
   private ArrayList<FileData> chunksToDeliver = new ArrayList<FileData>();	// Files to send.
   private ArrayList<FileData> incomingFiles = new ArrayList<FileData>();	// Files wanted.
   private ArrayList<FileData> currentSearchResults = new ArrayList<FileData>();	// Keeps info about current file search.

   final private int INVALID_IP_ADDRESS = 1;
   final private int INVALID_PORT = 2;
   final private int INVALID_PARAMETER = 3;
   final private int MISSING_PARAMETER = 4;

   public DataStore() {
	try {
	   localInetAddr = InetAddress.getLocalHost();
	} catch (UnknownHostException e) {
	   System.out.println("Failed to obtain local InetAddress!");
	}
   }

   /**
    *  The help to be returned to a user that requests it or fails calling the application. 
    */
   void showUsage() {
	System.out.println("\nUSAGE: StartP2P [Options]\n");
	System.out.println("Options:");
	System.out.println("\t-help\t\tShows this message.\n");
	System.out.println("\t-debug\t\tActivates debug mode.\n");
	System.out.println("\t-a ADDRESS\tDefines IP Address of Multicast channel. Default is 224.0.2.10.\n");
	System.out.println("\t-c CONTROLPORT\tDefines control port. Default is 8967. Valid is from 0 to 65535.\n");
	System.out.println("\t-d DATAPORT\tDefines data port. Default is 8967. Valid is from 0 to 65535.\n");
	System.out.println("\t-h HASHTYPE\tDefines hash type for files. Default is SHA-256, valid are:");
	for(HashTypes ht : HashTypes.values()) {
	   System.out.println("\t\t\t\t" + ht.name());
	}
	System.out.println("\n");
   }

   /**
    * Process args received from main.
    * @param args
    */
   public synchronized void processArgs(String[] args) {
	int i = 0;
	String arg;

	// The default value as in the requirements.
	try {
	   mcAddress = InetAddress.getByName("224.0.2.10");
	} catch (UnknownHostException e1) {
	   System.out.println("Something strange happened while converting a string to a InetAddress!");
	}

	while (i < args.length) {
	   arg = args[i++];
	   // Validates if a additional parameter needed is present (doesn't validate if it's the correct one).
	   if(i == args.length && (arg.equalsIgnoreCase("-a") || arg.equalsIgnoreCase("-c") || arg.equalsIgnoreCase("-d") || arg.equalsIgnoreCase("-h"))) {
		System.out.println("\nERROR: Missing parameter for option.");
		showUsage();
		System.exit(MISSING_PARAMETER);
	   }

	   if(arg.equalsIgnoreCase("-a")) {            // Change default multicast address.
		try {
		   mcAddress = InetAddress.getByName(args[i++]);
		} catch (UnknownHostException e) {
		   System.out.println("\nERROR: Invalid Internet Address for multicast: " + args[--i] + "\nPlease enter a valid one (x.y.z.w) where x, y, y, w in [0, 255]. Range 224.0.0.0 to 239.255.255.255");
		   showUsage();
		   System.exit(INVALID_IP_ADDRESS);
		}
	   }
	   else if(arg.equalsIgnoreCase("-c")) {		// Change default Control Port.
		try {
		   controlPort = Integer.parseInt(args[i++]);
		   if(controlPort < 0 || controlPort > 65535) {
			throw new NumberFormatException();
		   }
		} catch (NumberFormatException e) {
		   System.out.println("\nERROR: Invalid port: " + args[--i] + "\nPlease enter a valid one [0-65535], better not use well known ports (below 1023)!");
		   showUsage();
		   System.exit(INVALID_PORT);
		}
	   }
	   else if(arg.equalsIgnoreCase("-d")) {		// Change default Data Port.
		try {
		   dataPort = Integer.parseInt(args[i++]);
		   if(dataPort < 0 || dataPort > 65535) {
			throw new NumberFormatException();
		   }
		} catch (NumberFormatException e) {
		   System.out.println("\nERROR: Invalid port: " + args[--i] + "\nPlease enter a valid one [0-65535], better not use well known ports (below 1023)!");
		   showUsage();
		   System.exit(INVALID_PORT);
		}
	   }
	   else if(arg.equalsIgnoreCase("-h")) {		// Change type of hashing.
		for(HashTypes ht : HashTypes.values()) {
		   if(ht.toString().equalsIgnoreCase(args[i])) {
			hashType = ht;
		   }
		}
		i++;
	   }
	   else if(arg.equalsIgnoreCase("-debug")) {	 // Activate Debug mode.
		toDebug = true;
	   }
	   else if(arg.equalsIgnoreCase("-help")) {	 // Activate Debug mode.
		showUsage();
		System.exit(INVALID_PORT);
	   }
	   else {
		System.out.println("\nERROR: Invalid parameter found: " + arg);
		showUsage();
		System.exit(INVALID_PARAMETER);
	   }
	}

	// Used for debug.
	if(toDebug) {
	   System.out.println("\nValues to be used during execution of StartP2P:");
	   System.out.println("Multicast Address = " + mcAddress.toString());
	   System.out.println("Control Port = " + controlPort);
	   System.out.println("Data Port = " + dataPort);
	   System.out.println("Hash type = " + hashType.getFriendlyName());
	   System.out.println("Debug is active.");
	}
   }


   /**
    * @return the keepWorking
    */
   public synchronized void stopWorking() {
	working = false;
   }	

   /**
    * @return the keepWorking
    */
   public synchronized boolean keepWorking() {
	return working;
   }

   /**
    * @return the debug
    */
   public synchronized boolean debug() {
	return toDebug;
   }

   /**
    * @return the controlPort
    */
   public synchronized int getControlPort() {
	return controlPort;
   }

   /**
    * @return the dataPort
    */
   public synchronized int getDataPort() {
	return dataPort;
   }

   /**
    * @return the mcAddress
    */
   public synchronized InetAddress getMcAddress() {
	return mcAddress;
   }

   /**
    * @return the hashType
    */
   public synchronized HashTypes getHashType() {
	return hashType;
   }

   /**
    * @return the sharedDir
    */
   public synchronized File getSharedDir() {
	return sharedDir;
   }

   /**
    * Checks if a InetAddress is from the localhost.
    * @param sourceHost
    * @return
    */
   public synchronized boolean sentFromThisHost(InetAddress sourceHost) {
	if(sourceHost == localInetAddr) {
	   return true;
	}
	return false;
   }

   /**
    * Inserts informations about a new file requested.
    * @param fileHash
    * @param chunks
    * @return
    */
   public synchronized boolean newUploadStarted(String fileHash, String[] chunks) {
	FileData aFile = new FileData(fileHash);
	for(int i = 0; i < chunks.length; i++) {
	   aFile.addChunk(Integer.getInteger(chunks[i]));
	}
	return chunksToDeliver.add(aFile);
   }
   /**
    * Inserts informations about a new file requested.
    * @param fileHash
    * @param min
    * @param max
    * @return
    */
   public synchronized boolean newUploadStarted(String fileHash, int min, int max) {
	FileData aFile = new FileData(fileHash);
	for(int i = min; i < max; i++) {
	   aFile.addChunk(i);
	}
	return chunksToDeliver.add(aFile);
   }
   /**
    * Inserts informations about a new file requested.
    * @param fileHash
    * @param chunk
    * @return
    */
   public synchronized boolean newUploadStarted(String fileHash, int chunk) {
	FileData aFile = new FileData(fileHash);
	aFile.addChunk(chunk);
	return chunksToDeliver.add(aFile);
   }

   /**
    * Remove a chunk from the list of files to upload.
    * @param fileHash
    * @param chunkId
    * @return
    */
   public synchronized boolean chunkSent(String fileHash, long chunkId) {
	Iterator<FileData> itr = chunksToDeliver.iterator();
	FileData aFile;
	while(itr.hasNext()) {
	   aFile = itr.next();
	   if(aFile.getHash().equals(fileHash)) {
		return aFile.removeChunk(chunkId);
	   }
	}
	return false;
   }

   /**
    * Return a chunk ID from the list of chunks to deliver, -1 if list is empty.
    * @param fileHash
    * @return
    */
   public synchronized long chunkToSend(String fileHash) {
	Iterator<FileData> itr = chunksToDeliver.iterator();
	FileData aFile;
	while(itr.hasNext()) {
	   aFile = itr.next();
	   if(aFile.getHash().equals(fileHash)) {
		return aFile.giveMeAChunk();
	   }
	}
	return -1;
   }


   /**
    * Validates if a given File is a usable directory.
    * This code was copied from: http://www.javapractices.com/topic/TopicAction.do?Id=68
    * Under the Creative Commons license.
    * @param aDirectory
    * @throws FileNotFoundException
    */
   public synchronized void validateDirectory (File aDirectory) throws FileNotFoundException {
	if (aDirectory == null) {
	   throw new IllegalArgumentException("Directory should not be null.");
	}
	if (!aDirectory.exists()) {
	   throw new FileNotFoundException("Directory does not exist: " + aDirectory);
	}
	if (!aDirectory.isDirectory()) {
	   throw new IllegalArgumentException("Is not a directory: " + aDirectory);
	}
	if (!aDirectory.canRead()) {
	   throw new IllegalArgumentException("Directory cannot be read: " + aDirectory);
	}
	if (!aDirectory.canWrite()) {
	   throw new IllegalArgumentException("Directory cannot be writen: " + aDirectory);
	}
   }


   /**
    * Code adapted from the Assignment text.
    */
   public synchronized String getSHA256(File aFile) {
	MessageDigest md;
	String res = null;
	int nread = 0;
	byte[] dataBytes = new byte[1024];
	StringBuffer sb = new StringBuffer();

	try {
	   md = MessageDigest.getInstance("SHA-256");
	   FileInputStream fis = new FileInputStream(aFile);
	   while ((nread = fis.read(dataBytes)) != -1) {
		md.update(dataBytes, 0, nread);
	   }
	   byte[] mdbytes = md.digest();
	   for (int i = 0; i < mdbytes.length; i++) {
		sb.append(Integer.toString((mdbytes[i] & 0xff) + 0x100, 16).substring(1));
	   }
	   fis.close();
	   res = sb.toString();
	}
	catch (NoSuchAlgorithmException e) {
	   System.out.println("Algorithm doesn't exists!");
	}
	catch (IOException e) {
	   System.out.println("I/O Error while hashing a file!");
	}
	return res;
   }

   /**
    * Return information about what chunk is to send
    */
   public synchronized FileData giveMeWork() {
	return chunksToDeliver.get(0);	// Gives the oldest file.
   }

   /**
    * Clear results from previous search.
    */
   public synchronized void resetSearch(String searchID) {
	currentSearchResults.clear();
	currentSearch = searchID;
   }

   /**
    * Register a FOUND answer to a SEARCH.
    * @param controlPayload
    */
   public synchronized void processFound(String controlPayload) {	// FOUND id sha size filename
	String[] results = controlPayload.split(" ");
	if(results[1] == currentSearch) {
	   FileData aFile = new FileData(results[2]);
	   aFile.setFileSize(Long.getLong(results[3]));
	   aFile.setFilename(results[4]);
	   currentSearchResults.add(aFile);
	}	
   }

   public synchronized String[] getResults() {
	int results = currentSearchResults.size();
	String[] res = new String[results];
	for(int i = 0; i < results; i++) {
	   res[i] = currentSearchResults.get(i).getFilename(); 
	}
	return res;
   }

   /**
    * Insert a new file to download.
    * @param aFile
    */
   public synchronized void newDownload(int fileSelected) {
	long fileSize = currentSearchResults.get(fileSelected).getFileSize();
	long qtyChunks = currentSearchResults.get(fileSelected).getFileSize() / 1024;
	if(fileSize%1024 > 0) {
	   qtyChunks++;
	}
	for(int i = 0; i < qtyChunks; i++) {
	   currentSearchResults.get(fileSelected).addChunk(i + 1);
	}
	incomingFiles.add(currentSearchResults.get(fileSelected));
   }


   /**
    * Checks if there's at least one chunk to be sent. Does a little house keeping, by removing files with all chunks sent.
    * @return
    */
   public synchronized boolean existsWork() {
	if(chunksToDeliver.size() > 0) {
	   for(int i = 0; i < chunksToDeliver.size(); i++) {
		if(chunksToDeliver.get(i).giveMeAChunk() == -1) {
		   chunksToDeliver.remove(i);
		}
	   }
	   return true;
	}
	return false;
   }
}